//! Raycasting used for player object selection.

use craftdig_core::registrar::GameRegistrar;
use craftdig_datamodel::{block::model::Collider, world::WorldView};
use craftdig_math::{check_point_collision, BlkCoord, Direction, Vec3f};

#[derive(Copy, Clone, Debug)]
pub struct WorldRaycastHit {
    /// The precise point that was collided with.
    exact_pos: Vec3f,

    /// Face of the AABB that we hit.
    hit_face_dir: Direction,

    /// The target that was hit.
    target: RaycastTarget,
}

impl WorldRaycastHit {
    pub fn new(exact_pos: Vec3f, hit_face_dir: Direction, target: RaycastTarget) -> Self {
        Self {
            exact_pos,
            hit_face_dir,
            target,
        }
    }

    pub fn exact_pos(&self) -> Vec3f {
        self.exact_pos
    }

    pub fn hit_face_dir(&self) -> Direction {
        self.hit_face_dir
    }

    pub fn target(&self) -> RaycastTarget {
        self.target
    }

    /// Returns any reasonable block coord that could be ascribed to this hit.
    pub fn block_pos(&self) -> Option<BlkCoord> {
        match self.target {
            RaycastTarget::Block(blkcoord) => Some(blkcoord),
        }
    }
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RaycastTarget {
    /// Normal block selection.
    Block(BlkCoord),
}

/// Low-level raycast operation that checks against colliders and stuff.
///
/// If the raycast is too close to being zero length then we just check only the start and return as-is.
pub fn check_raycast(
    start: Vec3f,
    end: Vec3f,
    step_size: f32,
    wview: &impl WorldView,
    reg: &GameRegistrar,
) -> Option<WorldRaycastHit> {
    let span = end - start;

    // Early return to avoid weird shenanigans.
    if span.is_roughly_zero() {
        return check_point(start, wview, reg);
    }

    let steps = (span.magnitude() / step_size).floor() as usize;
    let step_diff = span.normalized() * step_size;

    for i in 0..steps {
        let pos = start + (step_diff * (i as f32));
        if let Some(hit) = check_point(pos, wview, reg) {
            return Some(hit);
        }
    }

    // Check the end point if we haven't hit by now.
    if let Some(hit) = check_point(end, wview, reg) {
        return Some(hit);
    }

    None
}

fn check_point(at: Vec3f, wview: &impl WorldView, reg: &GameRegistrar) -> Option<WorldRaycastHit> {
    let block_coord = BlkCoord::from_vec3f(at);
    let blkid = wview.get_block_id(block_coord)?;

    // If air then early return.
    if blkid.is_zero() {
        return None;
    }

    // Subtract our position relative to the block's origin, since the block's
    // collider has its own origin.
    let pos_rel = at - Vec3f::from(*block_coord.inner());

    let tydata = reg.block_ty_reg().get_by_id(blkid)?.tydata();
    match tydata.model().collider() {
        Collider::Empty => None,
        Collider::Cubic(aabb) => {
            if let Some(col) = check_point_collision(pos_rel, aabb) {
                let hit = WorldRaycastHit {
                    exact_pos: at,
                    hit_face_dir: col.travelling_dir().opposite(),
                    target: RaycastTarget::Block(block_coord),
                };

                Some(hit)
            } else {
                None
            }
        }
    }
}
